angular.module('ui.bootstrap.datepicker', ['ui.bootstrap.dateparser', 'ui.bootstrap.position'])

    .value('$datepickerSuppressError', false)

    .constant('uibDatepickerConfig', {
        formatDay: 'dd',
        formatMonth: 'MMMM',
        formatYear: 'yyyy',
        formatDayHeader: 'EEE',
        formatDayTitle: 'MMMM yyyy',
        formatMonthTitle: 'yyyy',
        datepickerMode: 'day',
        minMode: 'day',
        maxMode: 'year',
        showWeeks: true,
        startingDay: 0,
        yearRange: 20,
        minDate: null,
        maxDate: null,
        shortcutPropagation: false
    })

    .controller('UibDatepickerController', ['$scope', '$attrs', '$parse', '$interpolate', '$log', 'dateFilter', 'uibDatepickerConfig', '$datepickerSuppressError', function ($scope, $attrs, $parse, $interpolate, $log, dateFilter, datepickerConfig, $datepickerSuppressError) {
        var self = this,
            ngModelCtrl = {$setViewValue: angular.noop}; // nullModelCtrl;

        // Modes chain
        this.modes = ['day', 'month', 'year'];

        // Interpolated configuration attributes
        angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle'], function (key) {
            self[key] = angular.isDefined($attrs[key]) ? $interpolate($attrs[key])($scope.$parent) : datepickerConfig[key];
        });

        // Evaled configuration attributes
        angular.forEach(['showWeeks', 'startingDay', 'yearRange', 'shortcutPropagation'], function (key) {
            self[key] = angular.isDefined($attrs[key]) ? $scope.$parent.$eval($attrs[key]) : datepickerConfig[key];
        });

        // Watchable date attributes
        angular.forEach(['minDate', 'maxDate'], function (key) {
            if ($attrs[key]) {
                $scope.$parent.$watch($attrs[key], function (value) {
                    self[key] = value ? new Date(value) : null;
                    self.refreshView();
                });
            } else {
                self[key] = datepickerConfig[key] ? new Date(datepickerConfig[key]) : null;
            }
        });

        angular.forEach(['minMode', 'maxMode'], function (key) {
            if ($attrs[key]) {
                $scope.$parent.$watch($attrs[key], function (value) {
                    self[key] = $scope[key] = angular.isDefined(value) ? value : $attrs[key];
                    if (key === 'minMode' && self.modes.indexOf($scope.datepickerMode) < self.modes.indexOf(self[key]) ||
                        key === 'maxMode' && self.modes.indexOf($scope.datepickerMode) > self.modes.indexOf(self[key])) {
                        $scope.datepickerMode = self[key];
                    }
                });
            } else {
                self[key] = $scope[key] = datepickerConfig[key] || null;
            }
        });

        $scope.datepickerMode = $scope.datepickerMode || datepickerConfig.datepickerMode;
        $scope.uniqueId = 'datepicker-' + $scope.$id + '-' + Math.floor(Math.random() * 10000);

        if (angular.isDefined($attrs.initDate)) {
            this.activeDate = $scope.$parent.$eval($attrs.initDate) || new Date();
            $scope.$parent.$watch($attrs.initDate, function (initDate) {
                if (initDate && (ngModelCtrl.$isEmpty(ngModelCtrl.$modelValue) || ngModelCtrl.$invalid)) {
                    self.activeDate = initDate;
                    self.refreshView();
                }
            });
        } else {
            this.activeDate = new Date();
        }

        $scope.disabled = angular.isDefined($attrs.disabled) || false;
        if (angular.isDefined($attrs.ngDisabled)) {
            $scope.$parent.$watch($attrs.ngDisabled, function (disabled) {
                $scope.disabled = disabled;
                self.refreshView();
            });
        }

        $scope.isActive = function (dateObject) {
            if (self.compare(dateObject.date, self.activeDate) === 0) {
                $scope.activeDateId = dateObject.uid;
                return true;
            }
            return false;
        };

        this.init = function (ngModelCtrl_) {
            ngModelCtrl = ngModelCtrl_;

            ngModelCtrl.$render = function () {
                self.render();
            };
        };

        this.render = function () {
            if (ngModelCtrl.$viewValue) {
                var date = new Date(ngModelCtrl.$viewValue),
                    isValid = !isNaN(date);

                if (isValid) {
                    this.activeDate = date;
                } else if (!$datepickerSuppressError) {
                    $log.error('Datepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
                }
            }
            this.refreshView();
        };

        this.refreshView = function () {
            if (this.element) {
                this._refreshView();

                var date = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
                ngModelCtrl.$setValidity('dateDisabled', !date ||
                    this.element && !this.isDisabled(date));
            }
        };

        this.createDateObject = function (date, format) {
            var model = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : null;
            return {
                date: date,
                label: dateFilter(date, format),
                selected: model && this.compare(date, model) === 0,
                disabled: this.isDisabled(date),
                current: this.compare(date, new Date()) === 0,
                customClass: this.customClass(date)
            };
        };

        this.isDisabled = function (date) {
            return $scope.disabled ||
                this.minDate && this.compare(date, this.minDate) < 0 ||
                this.maxDate && this.compare(date, this.maxDate) > 0 ||
                $attrs.dateDisabled && $scope.dateDisabled({date: date, mode: $scope.datepickerMode});
        };

        this.customClass = function (date) {
            return $scope.customClass({date: date, mode: $scope.datepickerMode});
        };

        // Split array into smaller arrays
        this.split = function (arr, size) {
            var arrays = [];
            while (arr.length > 0) {
                arrays.push(arr.splice(0, size));
            }
            return arrays;
        };

        $scope.select = function (date) {
            if ($scope.datepickerMode === self.minMode) {
                var dt = ngModelCtrl.$viewValue ? new Date(ngModelCtrl.$viewValue) : new Date(0, 0, 0, 0, 0, 0, 0);
                dt.setFullYear(date.getFullYear(), date.getMonth(), date.getDate());
                ngModelCtrl.$setViewValue(dt);
                ngModelCtrl.$render();
            } else {
                self.activeDate = date;
                $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) - 1];
            }
        };

        $scope.move = function (direction) {
            var year = self.activeDate.getFullYear() + direction * (self.step.years || 0),
                month = self.activeDate.getMonth() + direction * (self.step.months || 0);
            self.activeDate.setFullYear(year, month, 1);
            self.refreshView();
        };

        $scope.toggleMode = function (direction) {
            direction = direction || 1;

            if ($scope.datepickerMode === self.maxMode && direction === 1 ||
                $scope.datepickerMode === self.minMode && direction === -1) {
                return;
            }

            $scope.datepickerMode = self.modes[self.modes.indexOf($scope.datepickerMode) + direction];
        };

        // Key event mapper
        $scope.keys = {
            13: 'enter',
            32: 'space',
            33: 'pageup',
            34: 'pagedown',
            35: 'end',
            36: 'home',
            37: 'left',
            38: 'up',
            39: 'right',
            40: 'down'
        };

        var focusElement = function () {
            self.element[0].focus();
        };

        // Listen for focus requests from popup directive
        $scope.$on('uib:datepicker.focus', focusElement);

        $scope.keydown = function (evt) {
            var key = $scope.keys[evt.which];

            if (!key || evt.shiftKey || evt.altKey || $scope.disabled) {
                return;
            }

            evt.preventDefault();
            if (!self.shortcutPropagation) {
                evt.stopPropagation();
            }

            if (key === 'enter' || key === 'space') {
                if (self.isDisabled(self.activeDate)) {
                    return; // do nothing
                }
                $scope.select(self.activeDate);
            } else if (evt.ctrlKey && (key === 'up' || key === 'down')) {
                $scope.toggleMode(key === 'up' ? 1 : -1);
            } else {
                self.handleKeyDown(key, evt);
                self.refreshView();
            }
        };
    }])

    .controller('UibDaypickerController', ['$scope', '$element', 'dateFilter', function (scope, $element, dateFilter) {
        var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

        this.step = {months: 1};
        this.element = $element;
        function getDaysInMonth(year, month) {
            return month === 1 && year % 4 === 0 &&
            (year % 100 !== 0 || year % 400 === 0) ? 29 : DAYS_IN_MONTH[month];
        }

        this.init = function (ctrl) {
            angular.extend(ctrl, this);
            scope.showWeeks = ctrl.showWeeks;
            ctrl.refreshView();
        };

        this.getDates = function (startDate, n) {
            var dates = new Array(n), current = new Date(startDate), i = 0, date;
            while (i < n) {
                date = new Date(current);
                dates[i++] = date;
                current.setDate(current.getDate() + 1);
            }
            return dates;
        };

        this._refreshView = function () {
            var year = this.activeDate.getFullYear(),
                month = this.activeDate.getMonth(),
                firstDayOfMonth = new Date(this.activeDate);

            firstDayOfMonth.setFullYear(year, month, 1);

            var difference = this.startingDay - firstDayOfMonth.getDay(),
                numDisplayedFromPreviousMonth = difference > 0 ?
                    7 - difference : -difference,
                firstDate = new Date(firstDayOfMonth);

            if (numDisplayedFromPreviousMonth > 0) {
                firstDate.setDate(-numDisplayedFromPreviousMonth + 1);
            }

            // 42 is the number of days on a six-week calendar
            var days = this.getDates(firstDate, 42);
            for (var i = 0; i < 42; i++) {
                days[i] = angular.extend(this.createDateObject(days[i], this.formatDay), {
                    secondary: days[i].getMonth() !== month,
                    uid: scope.uniqueId + '-' + i
                });
            }

            scope.labels = new Array(7);
            for (var j = 0; j < 7; j++) {
                scope.labels[j] = {
                    abbr: dateFilter(days[j].date, this.formatDayHeader),
                    full: dateFilter(days[j].date, 'EEEE')
                };
            }

            scope.title = dateFilter(this.activeDate, this.formatDayTitle);
            scope.rows = this.split(days, 7);

            if (scope.showWeeks) {
                scope.weekNumbers = [];
                var thursdayIndex = (4 + 7 - this.startingDay) % 7,
                    numWeeks = scope.rows.length;
                for (var curWeek = 0; curWeek < numWeeks; curWeek++) {
                    scope.weekNumbers.push(
                        getISO8601WeekNumber(scope.rows[curWeek][thursdayIndex].date));
                }
            }
        };

        this.compare = function (date1, date2) {
            var _date1 = new Date(date1.getFullYear(), date1.getMonth(), date1.getDate());
            var _date2 = new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
            _date1.setFullYear(date1.getFullYear());
            _date2.setFullYear(date2.getFullYear());
            return _date1 - _date2;
        };

        function getISO8601WeekNumber(date) {
            var checkDate = new Date(date);
            checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7)); // Thursday
            var time = checkDate.getTime();
            checkDate.setMonth(0); // Compare with Jan 1
            checkDate.setDate(1);
            return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
        }

        this.handleKeyDown = function (key, evt) {
            var date = this.activeDate.getDate();

            if (key === 'left') {
                date = date - 1;
            } else if (key === 'up') {
                date = date - 7;
            } else if (key === 'right') {
                date = date + 1;
            } else if (key === 'down') {
                date = date + 7;
            } else if (key === 'pageup' || key === 'pagedown') {
                var month = this.activeDate.getMonth() + (key === 'pageup' ? -1 : 1);
                this.activeDate.setMonth(month, 1);
                date = Math.min(getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth()), date);
            } else if (key === 'home') {
                date = 1;
            } else if (key === 'end') {
                date = getDaysInMonth(this.activeDate.getFullYear(), this.activeDate.getMonth());
            }
            this.activeDate.setDate(date);
        };
    }])

    .controller('UibMonthpickerController', ['$scope', '$element', 'dateFilter', function (scope, $element, dateFilter) {
        this.step = {years: 1};
        this.element = $element;

        this.init = function (ctrl) {
            angular.extend(ctrl, this);
            ctrl.refreshView();
        };

        this._refreshView = function () {
            var months = new Array(12),
                year = this.activeDate.getFullYear(),
                date;

            for (var i = 0; i < 12; i++) {
                date = new Date(this.activeDate);
                date.setFullYear(year, i, 1);
                months[i] = angular.extend(this.createDateObject(date, this.formatMonth), {
                    uid: scope.uniqueId + '-' + i
                });
            }

            scope.title = dateFilter(this.activeDate, this.formatMonthTitle);
            scope.rows = this.split(months, 3);
        };

        this.compare = function (date1, date2) {
            var _date1 = new Date(date1.getFullYear(), date1.getMonth());
            var _date2 = new Date(date2.getFullYear(), date2.getMonth());
            _date1.setFullYear(date1.getFullYear());
            _date2.setFullYear(date2.getFullYear());
            return _date1 - _date2;
        };

        this.handleKeyDown = function (key, evt) {
            var date = this.activeDate.getMonth();

            if (key === 'left') {
                date = date - 1;
            } else if (key === 'up') {
                date = date - 3;
            } else if (key === 'right') {
                date = date + 1;
            } else if (key === 'down') {
                date = date + 3;
            } else if (key === 'pageup' || key === 'pagedown') {
                var year = this.activeDate.getFullYear() + (key === 'pageup' ? -1 : 1);
                this.activeDate.setFullYear(year);
            } else if (key === 'home') {
                date = 0;
            } else if (key === 'end') {
                date = 11;
            }
            this.activeDate.setMonth(date);
        };
    }])

    .controller('UibYearpickerController', ['$scope', '$element', 'dateFilter', function (scope, $element, dateFilter) {
        var range;
        this.element = $element;

        function getStartingYear(year) {
            return parseInt((year - 1) / range, 10) * range + 1;
        }

        this.yearpickerInit = function () {
            range = this.yearRange;
            this.step = {years: range};
        };

        this._refreshView = function () {
            var years = new Array(range), date;

            for (var i = 0, start = getStartingYear(this.activeDate.getFullYear()); i < range; i++) {
                date = new Date(this.activeDate);
                date.setFullYear(start + i, 0, 1);
                years[i] = angular.extend(this.createDateObject(date, this.formatYear), {
                    uid: scope.uniqueId + '-' + i
                });
            }

            scope.title = [years[0].label, years[range - 1].label].join(' - ');
            scope.rows = this.split(years, 5);
        };

        this.compare = function (date1, date2) {
            return date1.getFullYear() - date2.getFullYear();
        };

        this.handleKeyDown = function (key, evt) {
            var date = this.activeDate.getFullYear();

            if (key === 'left') {
                date = date - 1;
            } else if (key === 'up') {
                date = date - 5;
            } else if (key === 'right') {
                date = date + 1;
            } else if (key === 'down') {
                date = date + 5;
            } else if (key === 'pageup' || key === 'pagedown') {
                date += (key === 'pageup' ? -1 : 1) * this.step.years;
            } else if (key === 'home') {
                date = getStartingYear(this.activeDate.getFullYear());
            } else if (key === 'end') {
                date = getStartingYear(this.activeDate.getFullYear()) + range - 1;
            }
            this.activeDate.setFullYear(date);
        };
    }])

    .directive('uibDatepicker', function () {
        return {
            replace: true,
            templateUrl: function (element, attrs) {
                return attrs.templateUrl || 'template/uib/datepicker/datepicker.html';
            },
            scope: {
                datepickerMode: '=?',
                dateDisabled: '&',
                customClass: '&',
                shortcutPropagation: '&?'
            },
            require: ['uibDatepicker', '^ngModel'],
            controller: 'UibDatepickerController',
            controllerAs: 'datepicker',
            link: function (scope, element, attrs, ctrls) {
                var datepickerCtrl = ctrls[0], ngModelCtrl = ctrls[1];

                datepickerCtrl.init(ngModelCtrl);
            }
        };
    })

    .directive('uibDaypicker', function () {
        return {
            replace: true,
            templateUrl: function (element, attrs) {
                return attrs.templateUrl || 'template/uib/datepicker/day.html';
            },
            require: ['^uibDatepicker', 'uibDaypicker'],
            controller: 'UibDaypickerController',
            link: function (scope, element, attrs, ctrls) {
                var datepickerCtrl = ctrls[0],
                    daypickerCtrl = ctrls[1];

                daypickerCtrl.init(datepickerCtrl);
            }
        };
    })

    .directive('uibMonthpicker', function () {
        return {
            replace: true,
            templateUrl: function (element, attrs) {
                return attrs.templateUrl || 'template/uib/datepicker/month.html';
            },
            require: ['^uibDatepicker', 'uibMonthpicker'],
            controller: 'UibMonthpickerController',
            link: function (scope, element, attrs, ctrls) {
                var datepickerCtrl = ctrls[0],
                    monthpickerCtrl = ctrls[1];

                monthpickerCtrl.init(datepickerCtrl);
            }
        };
    })

    .directive('uibYearpicker', function () {
        return {
            replace: true,
            templateUrl: function (element, attrs) {
                return attrs.templateUrl || 'template/uib/datepicker/year.html';
            },
            require: ['^uibDatepicker', 'uibYearpicker'],
            controller: 'UibYearpickerController',
            link: function (scope, element, attrs, ctrls) {
                var ctrl = ctrls[0];
                angular.extend(ctrl, ctrls[1]);
                ctrl.yearpickerInit();

                ctrl.refreshView();
            }
        };
    })

    .constant('uibDatepickerPopupConfig', {
        datepickerPopup: 'yyyy-MM-dd',
        datepickerPopupTemplateUrl: 'template/uib/datepicker/popup.html',
        datepickerTemplateUrl: 'template/uib/datepicker/datepicker.html',
        html5Types: {
            date: 'yyyy-MM-dd',
            'datetime-local': 'yyyy-MM-ddTHH:mm:ss.sss',
            'month': 'yyyy-MM'
        },
        currentText: 'Today',
        clearText: 'Clear',
        closeText: 'Done',
        closeOnDateSelection: true,
        appendToBody: false,
        showButtonBar: true,
        onOpenFocus: true,
        altInputFormats: []
    })

    .controller('UibDatepickerPopupController', ['$scope', '$element', '$attrs', '$compile', '$parse', '$document', '$rootScope', '$uibPosition', 'dateFilter', 'uibDateParser', 'uibDatepickerPopupConfig', '$timeout',
        function (scope, element, attrs, $compile, $parse, $document, $rootScope, $position, dateFilter, dateParser, datepickerPopupConfig, $timeout) {
            var self = this;
            var cache = {},
                isHtml5DateInput = false;
            var dateFormat, closeOnDateSelection, appendToBody, onOpenFocus,
                datepickerPopupTemplateUrl, datepickerTemplateUrl, popupEl, datepickerEl,
                ngModel, $popup, altInputFormats;

            scope.watchData = {};

            this.init = function (_ngModel_) {
                ngModel = _ngModel_;
                closeOnDateSelection = angular.isDefined(attrs.closeOnDateSelection) ? scope.$parent.$eval(attrs.closeOnDateSelection) : datepickerPopupConfig.closeOnDateSelection;
                appendToBody = angular.isDefined(attrs.datepickerAppendToBody) ? scope.$parent.$eval(attrs.datepickerAppendToBody) : datepickerPopupConfig.appendToBody;
                onOpenFocus = angular.isDefined(attrs.onOpenFocus) ? scope.$parent.$eval(attrs.onOpenFocus) : datepickerPopupConfig.onOpenFocus;
                datepickerPopupTemplateUrl = angular.isDefined(attrs.datepickerPopupTemplateUrl) ? attrs.datepickerPopupTemplateUrl : datepickerPopupConfig.datepickerPopupTemplateUrl;
                datepickerTemplateUrl = angular.isDefined(attrs.datepickerTemplateUrl) ? attrs.datepickerTemplateUrl : datepickerPopupConfig.datepickerTemplateUrl;
                altInputFormats = angular.isDefined(attrs.altInputFormats) ? scope.$parent.$eval(attrs.altInputFormats) : datepickerPopupConfig.altInputFormats;

                scope.showButtonBar = angular.isDefined(attrs.showButtonBar) ? scope.$parent.$eval(attrs.showButtonBar) : datepickerPopupConfig.showButtonBar;

                if (datepickerPopupConfig.html5Types[attrs.type]) {
                    dateFormat = datepickerPopupConfig.html5Types[attrs.type];
                    isHtml5DateInput = true;
                } else {
                    dateFormat = attrs.uibDatepickerPopup || datepickerPopupConfig.datepickerPopup;
                    attrs.$observe('uibDatepickerPopup', function (value, oldValue) {
                        var newDateFormat = value || datepickerPopupConfig.datepickerPopup;
                        // Invalidate the $modelValue to ensure that formatters re-run
                        // FIXME: Refactor when PR is merged: https://github.com/angular/angular.js/pull/10764
                        if (newDateFormat !== dateFormat) {
                            dateFormat = newDateFormat;
                            ngModel.$modelValue = null;

                            if (!dateFormat) {
                                throw new Error('uibDatepickerPopup must have a date format specified.');
                            }
                        }
                    });
                }

                if (!dateFormat) {
                    throw new Error('uibDatepickerPopup must have a date format specified.');
                }

                if (isHtml5DateInput && attrs.uibDatepickerPopup) {
                    throw new Error('HTML5 date input types do not support custom formats.');
                }

                // popup element used to display calendar
                popupEl = angular.element('<div uib-datepicker-popup-wrap><div uib-datepicker></div></div>');
                popupEl.attr({
                    'ng-model': 'date',
                    'ng-change': 'dateSelection(date)',
                    'template-url': datepickerPopupTemplateUrl
                });

                // datepicker element
                datepickerEl = angular.element(popupEl.children()[0]);
                datepickerEl.attr('template-url', datepickerTemplateUrl);

                if (isHtml5DateInput) {
                    if (attrs.type === 'month') {
                        datepickerEl.attr('datepicker-mode', '"month"');
                        datepickerEl.attr('min-mode', 'month');
                    }
                }

                if (attrs.datepickerOptions) {
                    var options = scope.$parent.$eval(attrs.datepickerOptions);
                    if (options && options.initDate) {
                        scope.initDate = options.initDate;
                        datepickerEl.attr('init-date', 'initDate');
                        delete options.initDate;
                    }
                    angular.forEach(options, function (value, option) {
                        datepickerEl.attr(cameltoDash(option), value);
                    });
                }

                angular.forEach(['minMode', 'maxMode', 'minDate', 'maxDate', 'datepickerMode', 'initDate', 'shortcutPropagation'], function (key) {
                    if (attrs[key]) {
                        var getAttribute = $parse(attrs[key]);
                        scope.$parent.$watch(getAttribute, function (value) {
                            scope.watchData[key] = value;
                            if (key === 'minDate' || key === 'maxDate') {
                                cache[key] = new Date(value);
                            }
                        });
                        datepickerEl.attr(cameltoDash(key), 'watchData.' + key);

                        // Propagate changes from datepicker to outside
                        if (key === 'datepickerMode') {
                            var setAttribute = getAttribute.assign;
                            scope.$watch('watchData.' + key, function (value, oldvalue) {
                                if (angular.isFunction(setAttribute) && value !== oldvalue) {
                                    setAttribute(scope.$parent, value);
                                }
                            });
                        }
                    }
                });

                if (attrs.dateDisabled) {
                    datepickerEl.attr('date-disabled', 'dateDisabled({ date: date, mode: mode })');
                }

                angular.forEach(['formatDay', 'formatMonth', 'formatYear', 'formatDayHeader', 'formatDayTitle', 'formatMonthTitle', 'showWeeks', 'startingDay', 'yearRange'], function (key) {
                    if (angular.isDefined(attrs[key])) {
                        datepickerEl.attr(cameltoDash(key), attrs[key]);
                    }
                });

                if (attrs.customClass) {
                    datepickerEl.attr('custom-class', 'customClass({ date: date, mode: mode })');
                }

                if (!isHtml5DateInput) {
                    // Internal API to maintain the correct ng-invalid-[key] class
                    ngModel.$$parserName = 'date';
                    ngModel.$validators.date = validator;
                    ngModel.$parsers.unshift(parseDate);
                    ngModel.$formatters.push(function (value) {
                        scope.date = value;
                        return ngModel.$isEmpty(value) ? value : dateFilter(value, dateFormat);
                    });
                } else {
                    ngModel.$formatters.push(function (value) {
                        scope.date = value;
                        return value;
                    });
                }

                // Detect changes in the view from the text box
                ngModel.$viewChangeListeners.push(function () {
                    scope.date = parseDateString(ngModel.$viewValue);
                });

                element.bind('keydown', inputKeydownBind);

                $popup = $compile(popupEl)(scope);
                // Prevent jQuery cache memory leak (template is now redundant after linking)
                popupEl.remove();

                if (appendToBody) {
                    $document.find('body').append($popup);
                } else {
                    element.after($popup);
                }

                scope.$on('$destroy', function () {
                    if (scope.isOpen === true) {
                        if (!$rootScope.$$phase) {
                            scope.$apply(function () {
                                scope.isOpen = false;
                            });
                        }
                    }

                    $popup.remove();
                    element.unbind('keydown', inputKeydownBind);
                    $document.unbind('click', documentClickBind);
                });
            };

            scope.getText = function (key) {
                return scope[key + 'Text'] || datepickerPopupConfig[key + 'Text'];
            };

            scope.isDisabled = function (date) {
                if (date === 'today') {
                    date = new Date();
                }

                return scope.watchData.minDate && scope.compare(date, cache.minDate) < 0 ||
                    scope.watchData.maxDate && scope.compare(date, cache.maxDate) > 0;
            };

            scope.compare = function (date1, date2) {
                return new Date(date1.getFullYear(), date1.getMonth(), date1.getDate()) - new Date(date2.getFullYear(), date2.getMonth(), date2.getDate());
            };

            // Inner change
            scope.dateSelection = function (dt) {
                if (angular.isDefined(dt)) {
                    scope.date = dt;
                }
                var date = scope.date ? dateFilter(scope.date, dateFormat) : null; // Setting to NULL is necessary for form validators to function
                element.val(date);
                ngModel.$setViewValue(date);

                if (closeOnDateSelection) {
                    scope.isOpen = false;
                    element[0].focus();
                }
            };

            scope.keydown = function (evt) {
                if (evt.which === 27) {
                    scope.isOpen = false;
                    element[0].focus();
                }
            };

            scope.select = function (date) {
                if (date === 'today') {
                    var today = new Date();
                    if (angular.isDate(scope.date)) {
                        date = new Date(scope.date);
                        date.setFullYear(today.getFullYear(), today.getMonth(), today.getDate());
                    } else {
                        date = new Date(today.setHours(0, 0, 0, 0));
                    }
                }
                scope.dateSelection(date);
            };

            scope.close = function () {
                scope.isOpen = false;
                element[0].focus();
            };

            scope.disabled = angular.isDefined(attrs.disabled) || false;
            if (attrs.ngDisabled) {
                scope.$parent.$watch($parse(attrs.ngDisabled), function (disabled) {
                    scope.disabled = disabled;
                });
            }

            scope.$watch('isOpen', function (value) {
                if (value) {
                    if (!scope.disabled) {
                        scope.position = appendToBody ? $position.offset(element) : $position.position(element);
                        scope.position.top = scope.position.top + element.prop('offsetHeight');

                        $timeout(function () {
                            if (onOpenFocus) {
                                scope.$broadcast('uib:datepicker.focus');
                            }
                            $document.bind('click', documentClickBind);
                        }, 0, false);
                    } else {
                        scope.isOpen = false;
                    }
                } else {
                    $document.unbind('click', documentClickBind);
                }
            });

            function cameltoDash(string) {
                return string.replace(/([A-Z])/g, function ($1) {
                    return '-' + $1.toLowerCase();
                });
            }

            function parseDateString(viewValue) {
                var date = dateParser.parse(viewValue, dateFormat, scope.date);
                if (isNaN(date)) {
                    for (var i = 0; i < altInputFormats.length; i++) {
                        date = dateParser.parse(viewValue, altInputFormats[i], scope.date);
                        if (!isNaN(date)) {
                            return date;
                        }
                    }
                }
                return date;
            }

            function parseDate(viewValue) {
                if (angular.isNumber(viewValue)) {
                    // presumably timestamp to date object
                    viewValue = new Date(viewValue);
                }

                if (!viewValue) {
                    return null;
                }

                if (angular.isDate(viewValue) && !isNaN(viewValue)) {
                    return viewValue;
                }

                if (angular.isString(viewValue)) {
                    var date = parseDateString(viewValue);
                    if (!isNaN(date)) {
                        return date;
                    }
                }

                return ngModel.$options && ngModel.$options.allowInvalid ? viewValue : undefined;
            }

            function validator(modelValue, viewValue) {
                var value = modelValue || viewValue;

                if (!attrs.ngRequired && !value) {
                    return true;
                }

                if (angular.isNumber(value)) {
                    value = new Date(value);
                }

                if (!value) {
                    return true;
                }

                if (angular.isDate(value) && !isNaN(value)) {
                    return true;
                }

                if (angular.isString(value)) {
                    return !isNaN(parseDateString(viewValue));
                }

                return false;
            }

            function documentClickBind(event) {
                if (!scope.isOpen && scope.disabled) {
                    return;
                }

                var popup = $popup[0];
                var dpContainsTarget = element[0].contains(event.target);
                // The popup node may not be an element node
                // In some browsers (IE) only element nodes have the 'contains' function
                var popupContainsTarget = popup.contains !== undefined && popup.contains(event.target);
                if (scope.isOpen && !(dpContainsTarget || popupContainsTarget)) {
                    scope.$apply(function () {
                        scope.isOpen = false;
                    });
                }
            }

            function inputKeydownBind(evt) {
                if (evt.which === 27 && scope.isOpen) {
                    evt.preventDefault();
                    evt.stopPropagation();
                    scope.$apply(function () {
                        scope.isOpen = false;
                    });
                    element[0].focus();
                } else if (evt.which === 40 && !scope.isOpen) {
                    evt.preventDefault();
                    evt.stopPropagation();
                    scope.$apply(function () {
                        scope.isOpen = true;
                    });
                }
            }
        }])

    .directive('uibDatepickerPopup', function () {
        return {
            require: ['ngModel', 'uibDatepickerPopup'],
            controller: 'UibDatepickerPopupController',
            scope: {
                isOpen: '=?',
                currentText: '@',
                clearText: '@',
                closeText: '@',
                dateDisabled: '&',
                customClass: '&'
            },
            link: function (scope, element, attrs, ctrls) {
                var ngModel = ctrls[0],
                    ctrl = ctrls[1];

                ctrl.init(ngModel);
            }
        };
    })

    .directive('uibDatepickerPopupWrap', function () {
        return {
            replace: true,
            transclude: true,
            templateUrl: function (element, attrs) {
                return attrs.templateUrl || 'template/uib/datepicker/popup.html';
            }
        };
    });